package com.gee.spring.boot.influxdb.core.util;

import com.gee.spring.boot.influxdb.annotations.Column;
import com.gee.spring.boot.influxdb.core.convertor.CommonConvertor;
import com.gee.spring.boot.influxdb.core.convertor.Convertor;
import com.gee.spring.boot.influxdb.core.query.domain.ColumnInfo;
import com.gee.spring.boot.influxdb.core.query.domain.MeasurementInfo;
import org.influxdb.InfluxDBMapperException;
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.Point;
import org.influxdb.dto.QueryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.gee.spring.boot.influxdb.core.util.ReflectionUtil.*;
import static com.gee.spring.boot.influxdb.core.util.TimeUtil.nanosToTime;
import static com.gee.spring.boot.influxdb.core.util.TimeUtil.timeToNanos;

/**
 * @author gepengjun
 * @since 2024/4/24 14:16
 */
public class InfluxdbUtil {

    private static final Logger logger = LoggerFactory.getLogger(InfluxdbUtil.class);

    public static final BigInteger NANOSECONDS_PER_SECOND = BigInteger.valueOf(1000000000L);
    //className MeasurementInfo
    public static final ConcurrentHashMap<String, MeasurementInfo> MeasurementInfoMap = new ConcurrentHashMap<>();

    //className Convertor
    public static final ConcurrentHashMap<String, Convertor> ConvertorMap = new ConcurrentHashMap<>();


    /**
     * 将queryResult 转换为实体对象List
     * @param queryResult queryResult
     * @param clazz clazz
     * @return List<T>
     */
    public static <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz) {
        return toPOJO(queryResult, clazz, false);
    }

    public static <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz, boolean funcTime) {
        Objects.requireNonNull(queryResult, "queryResult");
        Objects.requireNonNull(clazz, "clazz");
        List<T> results = new LinkedList<T>();
        queryResult.getResults().stream()
                .filter(internalResult -> Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries()))
                .forEach(internalResult -> {
                    internalResult.getSeries().stream()
                            .forEachOrdered(series -> {
                                parsedSeries(series, clazz, results, funcTime);
                            });
                });
        return results;
    }

    private static <T> void parsedSeries(QueryResult.Series series, Class<T> clazz, List<T> results, boolean funcTime) {
        int columnSize = series.getColumns().size();
        MeasurementInfo measurementInfo = getMeasurementInfo(clazz);
        try {
            T object = null;
            for (List<Object> row : series.getValues()) {
                for (int i = 0; i < columnSize; i++) {
                    String columnName = series.getColumns().get(i);
                    if (object == null) {
                        object = newInstance(clazz);
                    }
                    if (funcTime){
                        if (columnName.equals(measurementInfo.getTimeFuncAlias())){
                            Field timeField = measurementInfo.getTimeField();
                            ReflectionUtil.setFieldValue(object, timeField, nanosToTime((Long) transformFieldValue(row.get(i), Long.class)));
                            continue;
                        }
                        if ("time".equals(columnName)){
                            continue;
                        }
                    }
                    Field correspondingField = measurementInfo.getField(columnName);
                    if (correspondingField != null) {
                        setFieldValue(object, correspondingField, row.get(i));
                    }
                }
                // When the "GROUP BY" clause is used, "tags" are returned as Map<String,String> and
                // accordingly with InfluxDB documentation
                // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value
                // "tag" values are always String.
                if (series.getTags() != null && !series.getTags().isEmpty()) {
                    for (Map.Entry<String, String> entry : series.getTags().entrySet()) {
                        Field correspondingField = measurementInfo.getField(entry.getKey()/*InfluxDB columnName*/);
                        if (correspondingField != null) {
                            // I don't think it is possible to reach here without a valid "object"
                            setFieldValue(object, correspondingField, entry.getValue());
                        }
                    }
                }
                if (object != null) {
                    results.add(object);
                    object = null;
                }
            }
        } catch (IllegalAccessException e) {
            throw new InfluxDBMapperException(e);
        }
    }

    private static void setFieldValue(Object obj, Field field, Object value) throws IllegalAccessException {
        Column column = field.getAnnotation(Column.class);
        if (column.convertorClass() == CommonConvertor.class){
            field.set(obj, transformFieldValue(value, field.getType()));
        }else {
            Convertor convertor = getConvertor(column.convertorClass());
            Type saveType = getSaveType(column.convertorClass());
            field.set(obj,convertor.toJavaValue(transformFieldValue(value, saveType)));
        }
    }

    public static long getCount(QueryResult queryResult){
        Object countObj = queryResult.getResults().get(0).getSeries().get(0).getValues().get(0).get(1);
        return Double.valueOf(countObj.toString()).longValue();
    }

    private static Object transformFieldValue(Object value, Type type){
        if (type.equals(Long.class)) {
            return  Double.valueOf(value.toString()).longValue();
        } else if (type.equals(Integer.class)) {
            return  Double.valueOf(value.toString()).intValue();
        } else if (type.equals(Float.class)) {
            return  Double.valueOf(value.toString()).floatValue();
        } else if (type.equals(Double.class)) {
            return  Double.valueOf(value.toString());
        } else if (type.equals(Boolean.class)) {
            return Boolean.valueOf(value.toString());
        }else if (type.equals(BigDecimal.class)) {
            return new BigDecimal(value.toString());
        } else if (type.equals(LocalDateTime.class)) {
            return parseStringToLocalDateTime(value.toString());
        } else if (type.equals(String.class)) {
            return value.toString();
        } else {
            throw new RuntimeException("common convertor unsupported field type: " + type.getTypeName());
        }
    }

    /**
     * 赋值
     *
     * @param obj
     * @param field
     * @param value
     * @throws IllegalAccessException
     */


    /**
     * 时间转换 Instant字符串时间转 LocalDateTime
     *
     * @param time
     * @return
     */
    public static LocalDateTime parseStringToLocalDateTime(String time) {
        DateTimeFormatter df = DateTimeFormatter.ISO_DATE_TIME;
        return LocalDateTime.parse(time, df).plus(8, ChronoUnit.HOURS);
    }

    public static Point toPoint(Object object) {
        Class<?> clazz = object.getClass();
        throwExceptionIfMissingAnnotation(clazz);
        MeasurementInfo measurementInfo = getMeasurementInfo(object.getClass());
        List<Field> fieldList = measurementInfo.getFieldColumnInfoMap().values().stream()
                .map(ColumnInfo::getField)
                .filter(field -> getFieldValue(object, field) != null)
                .collect(Collectors.toList());
        Point.Builder builder = Point.measurement(measurementInfo.getMeasurement());
        for (Field field: fieldList){
            try {
                Column column = field.getAnnotation(Column.class);
                if ("time".equals(column.name())){
                    builder.time(timeToNanos((LocalDateTime) field.get(object)), TimeUnit.NANOSECONDS);
                    continue;
                }
                if (column.tag()){
                    builder.tag(column.name(), field.get(object).toString());
                }else {
                    Class<? extends Convertor> convertorClass = column.convertorClass();
                    if (convertorClass == CommonConvertor.class){
                        builder.field(column.name(), field.get(object));
                    }else {
                        Object saveValue = AppContextUtil.getConvertor(convertorClass).toSaveValue(field.get(object));
                        builder.field(column.name(), saveValue);
                    }
                }
            } catch (IllegalAccessException e) {
                logger.error("to point error, field: {}, object: {}", field.getName(), object);
            }
        }
        return builder.build();
    }


    private static void throwExceptionIfMissingAnnotation(final Class<?> clazz) {
        if (!clazz.isAnnotationPresent(Measurement.class)) {
            throw new IllegalArgumentException(
                    "Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName());
        }
    }

    public static MeasurementInfo getMeasurementInfo(Class<?> entityClass){
        return MeasurementInfoMap.computeIfAbsent(entityClass.getName(), className -> {
            Measurement measurement = entityClass.getAnnotation(Measurement.class);
            if (measurement == null) {
                throw ExceptionUtil.re("class named %s needs an annotation with %s", className, Measurement.class.getName());
            }
            MeasurementInfo measurementInfo = new MeasurementInfo(measurement.name());
            Map<String, ColumnInfo> fieldColumnInfoMap = measurementInfo.getFieldColumnInfoMap();
            Map<String, String> columnFieldMap = measurementInfo.getColumnFieldMap();
            Class<?> c = entityClass;
            while (c != null) {
                for (Field field : c.getDeclaredFields()) {
                    Column column = field.getAnnotation(Column.class);
                    if (column != null){
                        //初始化时设置， 其他地方不再设置
                        field.setAccessible(true);
                        fieldColumnInfoMap.put(field.getName(), new ColumnInfo(field, column,
                                column.convertorClass().getName().equals(CommonConvertor.class.getName())
                                        ? field.getType()
                                        : (Class<?>) getSaveType(column.convertorClass()
                                        )
                        ));
                        columnFieldMap.put(column.name(),field.getName());
                        if ("time".equals(column.name())){
                            measurementInfo.setTimeFuncAlias(column.funcAlias());
                            measurementInfo.setTimeField(field);
                        }
                    }
                }
                c = c.getSuperclass();
            }
            return measurementInfo;
        });
    }

    public static Convertor getConvertor(Class<? extends Convertor> convertorClass){
        return ConvertorMap.computeIfAbsent(convertorClass.getName(), className -> newInstance(convertorClass));
    }
}
